進入到user的註冊與登錄,因為我們已經把所有的路徑都開給blog用了,account如果要使用其他api路徑有兩種方法
使用第2種方法需要開其他router同時運行,要讓request對應到不同的router處理一樣有兩種方法
我這裡使用第2種方法,之後靠nginx來導引到正確的port,如果不使用代理工具就只能用第一種方法了,作法在第二天的文章可以看到
先寫設定,打開config/app/app.yaml寫入
servers:
main: &main_server
host: your_host
port: 8000
RunMode: debug
ReadTimeout: 60s
WriteTimeout: 60s
FilePath: your_file_path # path of user file
LogPath: your_log_path # path of log file
account:
<<: *main_server
port: 8001
解釋:
&main_server
註冊一個名為main_server的錨點,可以繼承main以下的資料<<: *main_server
引用main_server,下面的port代表只改port,其他port與main相同改寫router/host_switch.go的HostSwitch為
type HostSwitch struct {
*gin.Engine
}
// Implement the ServeHTTP method on our new type
func (hs HostSwitch) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Check if a http.Handler is registered for the given host.
// If yes, use it to handle the request.
// clean path
path := cleanPath(r.URL.Path)
// update to request
r2 := new(http.Request)
*r2 = *r
r2.URL = new(url.URL)
*r2.URL = *r.URL
r2.URL.Path = path
hs.Engine.ServeHTTP(w, r2)
}
改寫main.go為
package main
import (
"app/router"
"app/setting"
"log"
"net/http"
"fmt"
"golang.org/x/sync/errgroup"
)
var (
g errgroup.Group
)
func main() {
runServe("main", router.HostSwitch{Engine: router.MainRouter()})
runServe("account", router.HostSwitch{Engine: router.AccountRouter()})
if err := g.Wait(); err != nil {
log.Fatal(err)
}
}
func runServe(serve string, hs router.HostSwitch) {
s := &http.Server{
Addr: fmt.Sprintf(":%d", setting.Servers[serve].Port),
Handler: hs,
ReadTimeout: setting.Servers[serve].ReadTimeout,
WriteTimeout: setting.Servers[serve].WriteTimeout,
}
g.Go(func() error {
return s.ListenAndServe()
})
}
解釋:
來寫router,在router創建account.go寫入
package router
import (
"app/middleware"
"app/serve"
"app/setting"
"github.com/gin-gonic/gin"
)
func AccountRouter() *gin.Engine {
r := gin.New()
gin.SetMode(setting.Servers["account"].RunMode)
r.Use(middleware.Logging())
r.Use(middleware.ErrorHandle())
r.LoadHTMLGlob("view/html/*/*")
r.PUT("/signup", serve.PutUser)
return r
}
解釋:
要存使用者的帳號密碼不能直接存明文,不然資料庫外洩使用的丈密碼上就被看光光了,一般都會把密碼hash過後存起來,但是這樣還有個問題,如果駭客先將所有輸入的結果的hash值存起來了,就能反向回推出密碼,這被稱之為彩虹表攻擊,為了避免這狀況我們還要加上salt(一組隨機值)來擾亂輸入值。
現在計算的速度越來越快,上述的方法在brute force的攻擊下還是很危險,所以學術界有發展出一套密碼用的hash方法,有名的有bcrypt、scrypt與現在最新的argon2,主要的概念與一般sha等hash背道而馳,密碼上的hash運算速度必須要慢,從使用者的角度來看,登錄多花一秒的時間不會感覺太大,但如果駭客想用暴力破解,他要猜出一組密碼就必須要多跑到上萬秒以上,再來要使用下一代的hash演算法argon2
argon2是2015的Password Hashing Competition冠軍,利用快速填充記憶體空間來防禦利用GPU等進行硬體加速計算,go已經有支持了,不需要自己刻一個。
我們先在util建立random package,創建random.go寫入
package random
import (
"crypto/rand"
"encoding/base64"
)
// GenerateRandomBytes returns securely generated random bytes.
// It will return an error if the system's secure random
// number generator fails to function correctly
func GetRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
// Note that err == nil only if we read len(b) bytes.
if err != nil {
return nil, err
}
return b, nil
}
// GenerateRandomString returns a URL-safe, base64 encoded
// securely generated random string.
func GetRandomString(s int) (string, error) {
b, err := GetRandomBytes(s)
return base64.RawURLEncoding.EncodeToString(b), err
}
接著一樣在util底下建立hash package創建hash.go寫入
package hash
import (
"app/util/random"
"encoding/base64"
"golang.org/x/crypto/argon2"
"strings"
)
// get hash of argon2 for password hash
func NewPWHash(pw string, time, memory uint32, threads uint8, keyLen uint32) ([]byte, []byte, error) {
salt, err := random.GetRandomBytes(16)
if err != nil {
return nil, nil, err
}
return argon2.IDKey([]byte(pw), salt, time, memory, threads, keyLen), salt, nil
}
// generate new hash string in base64 url raw encode with salt of argon2 for password hash
func NewPWHashString(pw string, time, memory uint32, threads uint8, keyLen uint32) (string, string, error) {
hash, salt, err := NewPWHash(pw, time, memory, threads, keyLen)
if err != nil {
return "", "", nil
}
return base64.RawURLEncoding.EncodeToString(hash), base64.RawURLEncoding.EncodeToString(salt), nil
}
// get hash string in base64 url raw encode of argon2 for password hash
func GetPWHashString(pw, salt string, time, memory uint32, threads uint8, keyLen uint32) (string, error) {
saltByte, err := base64.RawURLEncoding.DecodeString(salt)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(argon2.IDKey([]byte(pw), saltByte, time, memory, threads, keyLen)), nil
}
解釋:
NewPWHashString
建立一組新的hash string,與對應的saltGetPWHashString
將salt與密碼逕行hash後回傳結果在serve package創建account.go寫入
package serve
import (
"app/apperr"
"app/database"
"app/log"
"app/setting"
"app/util/hash"
"github.com/gin-gonic/gin"
"net/http"
)
// set for hash parameter
var Params = struct {
memory uint32
iterations uint32
parallelism uint8
saltLength uint32
keyLength uint32
}{
memory: 65536,
iterations: 10,
parallelism: 2,
saltLength: 16,
keyLength: 32,
}
// insert an user if oid is 0 else update
func PutUser(c *gin.Context) {
pw, salt, err := hash.NewPWHashString(c.PostForm("password"), Params.iterations, Params.memory, Params.parallelism, Params.keyLength)
if err != nil {
log.Error(c, apperr.ErrPermissionDenied, err, 0, "Sorry, something error", "rand function error")
return
}
err = database.PutUser(c.PostForm("uid"), c.PostForm("username"), pw, c.PostForm("email"), salt)
if err != nil {
log.Warn(c, apperr.ErrWrongArgument, err, "sorry, something error. try again", "insert new user fail")
return
}
c.Redirect(http.StatusSeeOther, setting.Servers["main"].Host+strconv.Itoa(setting.Servers["main"].Port))
}
在database/account.go寫入
// insert an user if uid is 0 else update
func PutUser(uid, username, password, email, salt string) error {
return checkAffect(db.Exec("call put_user(?, ?, ?, ?, ?)", uid, username, password, email, salt))
}
再來寫procedure,進到資料庫後輸入
DELIMITER ;;
CREATE PROCEDURE `put_user`(
userid INT UNSIGNED,
user_name VARCHAR(50),
password CHAR(44),
email varchar(40),
salt CHAR(22)
)
BEGIN
IF userid = 0 THEN
INSERT INTO `user` (`uid`, `username`, `password`, `email`, `salt`) VALUES (userid, user_name, password, email, salt);
ELSE
UPDATE `user`
SET `user`.`password` = password, `user`.`email` = email, `user`.`salt` = salt
WHERE `user`.`uid` = userid AND `user`.`username` = user_name;
END IF;
END ;;
DELIMITER ;
使用者能註冊了,明天寫登錄與權限控管
目前的工作環境
.
├── app
│ ├── apperr
│ │ ├── error.go
│ │ └── handle.go
│ ├── common
│ │ └── cookie.go
│ ├── config
│ │ └── app
│ │ ├── app.yaml
│ │ └── error.yaml
│ ├── database
│ │ ├── connect.go
│ │ ├── error.go
│ │ ├── main.go
│ │ └── scheme.go
│ ├── go.mod
│ ├── go.sum
│ ├── log
│ │ ├── logger.go
│ │ └── logging.go
│ ├── main.go
│ ├── middleware
│ │ ├── error.go
│ │ └── log.go
│ ├── router
│ │ ├── account.go
│ │ ├── host_switch.go
│ │ └── main.go
│ ├── serve
│ │ ├── account.go
│ │ ├── main.go
│ │ └── main_test.go
│ ├── setting
│ │ └── setting.go
│ ├── util
│ │ ├── debug
│ │ │ ├── stack.go
│ │ │ └── stack_test.go
│ │ ├── file
│ │ │ └── file.go
│ │ ├── hash
│ │ │ ├── hash.go
│ │ │ └── hash_test.go
│ │ └── random
│ │ └── random.go
│ └── view
│ ├── css
│ ├── html
│ │ ├── component
│ │ │ ├── blogContainer.html
│ │ │ └── blogList.html
│ │ └── meta
│ │ ├── head.html
│ │ └── index.html
│ └── js
├── config
│ └── app
│ ├── app.yaml
│ └── error.yaml
└── database
└── maindata